#pragma once

#include "JsiDomDrawingNode.h"
#include "PathProp.h"

#include <algorithm>
#include <memory>
#include <string>

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"

#include "include/effects/SkTrimPathEffect.h"

#pragma clang diagnostic pop

namespace RNSkia {
static PropId PropNameMiterLimit = JsiPropId::get("miter_limit");
static PropId PropNamePrecision = JsiPropId::get("precision");

class JsiPathNode : public JsiDomDrawingNode, public JsiDomNodeCtor<JsiPathNode> {
public:
    explicit JsiPathNode(std::shared_ptr<RNSkPlatformContext> context) : JsiDomDrawingNode(context, "skPath") {}

protected:
    void draw(DrawingContext *context) override
    {
        if (getPropsContainer()->isChanged()) {
            auto start = saturate(_startProp->isSet() ? _startProp->value().getAsNumber() : 0.0);
            auto end = saturate(_endProp->isSet() ? _endProp->value().getAsNumber() : 1.0);
            // Can we use the path directly, or do we need to copy to
            // mutate / modify the path?
            auto hasStartOffset = start != 0.0;
            auto hasEndOffset = end != 1.0;
            auto hasFillStyle = _fillTypeProp->isSet();
            auto hasStrokeOptions = _strokeOptsProp->isSet() && _strokeOptsProp->value().getType() == PropType::Object;

            auto willMutatePath =
                hasStartOffset == true || hasEndOffset == true || hasFillStyle == true || hasStrokeOptions == true;

            if (willMutatePath) {
                // We'll trim the path
                SkPath filteredPath(*_pathProp->getDerivedValue());
                auto pe = SkTrimPathEffect::Make(start, end, SkTrimPathEffect::Mode::kNormal);

                if (pe != nullptr) {
                    SkStrokeRec rec(SkStrokeRec::InitStyle::kHairline_InitStyle);
                    if (!pe->filterPath(&filteredPath, filteredPath, &rec, nullptr)) {
                        throw std::runtime_error("Failed trimming path with parameters start: " +
                            std::to_string(start) + ", end: " + std::to_string(end));
                    }
                    filteredPath.swap(filteredPath);
                    _path = std::make_shared<const SkPath>(filteredPath);
                } else if (hasStartOffset || hasEndOffset) {
                    throw std::runtime_error("Failed trimming path with parameters start: " + std::to_string(start) +
                        ", end: " + std::to_string(end));
                } else {
                    _path = std::make_shared<const SkPath>(filteredPath);
                }

                // Set fill style
                if (_fillTypeProp->isSet()) {
                    auto fillType = _fillTypeProp->value().getAsString();
                    auto p = std::make_shared<SkPath>(*_path.get());
                    p->setFillType(getFillTypeFromStringValue(fillType));
                    _path = std::const_pointer_cast<const SkPath>(p);
                }

                // do we have a special paint here?
                if (_strokeOptsProp->isSet()) {
                    auto opts = _strokeOptsProp->value();
                    SkPaint strokePaint;

                    if (opts.hasValue(JsiPropId::get("strokeCap"))) {
                        strokePaint.setStrokeCap(
                            StrokeCapProp::getCapFromString(opts.getValue(JsiPropId::get("strokeCap")).getAsString()));
                    }

                    if (opts.hasValue(JsiPropId::get("strokeJoin"))) {
                        strokePaint.setStrokeJoin(StrokeJoinProp::getJoinFromString(
                            opts.getValue(JsiPropId::get("strokeJoin")).getAsString()));
                    }

                    if (opts.hasValue(PropNameWidth)) {
                        strokePaint.setStrokeWidth(opts.getValue(PropNameWidth).getAsNumber());
                    }

                    if (opts.hasValue(PropNameMiterLimit)) {
                        strokePaint.setStrokeMiter(opts.getValue(PropNameMiterLimit).getAsNumber());
                    }

                    double precision = 1.0;
                    if (opts.hasValue(PropNamePrecision)) {
                        precision = opts.getValue(PropNamePrecision).getAsNumber();
                    }

                    // _path is const so we can't mutate it directly, let's replace the
                    // path like this:
                    auto p = std::make_shared<SkPath>(*_path.get());
                    if (!skpathutils::FillPathWithPaint(*_path.get(), strokePaint, p.get(), nullptr, precision)) {
                        _path = nullptr;
                    } else {
                        _path = std::const_pointer_cast<const SkPath>(p);
                    }
                }
            } else {
                // We'll just draw the pure path
                _path = _pathProp->getDerivedValue();
            }
        }

        if (_path == nullptr) {
            throw std::runtime_error("Path node could not resolve path props correctly.");
        }

        context->getCanvas()->drawPath(*_path, *context->getPaint());
    }

    void defineProperties(NodePropsContainer *container) override
    {
        JsiDomDrawingNode::defineProperties(container);
        _pathProp = container->defineProperty<PathProp>("path");
        _startProp = container->defineProperty<NodeProp>("start");
        _endProp = container->defineProperty<NodeProp>("end");
        _fillTypeProp = container->defineProperty<NodeProp>("fillType");
        _strokeOptsProp = container->defineProperty<NodeProp>("stroke");

        _pathProp->require();
    }

private:
    float saturate(float x)
    {
        return std::max(0.0f, std::min(1.0f, x));
    }

    SkPathFillType getFillTypeFromStringValue(const std::string &value)
    {
        if (value == "winding") {
            return SkPathFillType::kWinding;
        } else if (value == "evenOdd") {
            return SkPathFillType::kEvenOdd;
        } else if (value == "inverseWinding") {
            return SkPathFillType::kInverseWinding;
        } else if (value == "inverseEvenOdd") {
            return SkPathFillType::kInverseEvenOdd;
        }
        throw std::runtime_error("Could not convert value \"" + value + "\" to path fill type.");
    }

    PathProp *_pathProp;
    NodeProp *_startProp;
    NodeProp *_endProp;
    NodeProp *_fillTypeProp;
    NodeProp *_strokeOptsProp;

    std::shared_ptr<const SkPath> _path;
};

class StrokeOptsProps : public BaseDerivedProp {
public:
    explicit StrokeOptsProps(const std::function<void(BaseNodeProp *)> &onChange) : BaseDerivedProp(onChange)
    {
        _strokeProp = defineProperty<NodeProp>("stroke");
    }

private:
    NodeProp *_strokeProp;
};
} // namespace RNSkia
